﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.Reflection;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using System.Linq;

namespace DynamicProxyCore
{
	public static class DynamicProxy
	{
		private class InternalDynamicProxy<T> : DynamicProxy<T>
			where T : class
		{
			public InternalDynamicProxy([NotNull] T obj)
				: base(obj)
			{
			}

			protected override DynamicProxy<T> CreateProxy(T obj)
			{
				return new InternalDynamicProxy<T>(obj);
			}

			public static implicit operator T(InternalDynamicProxy<T> proxy)
			{
				return proxy.Value;
			}

			public static explicit operator InternalDynamicProxy<T>(T obj)
			{
				return new InternalDynamicProxy<T>(obj);
			}
		}

		public static DynamicProxy<TProxyType> Create<TProxyType>(TProxyType obj)
			where TProxyType : class
		{
			return new InternalDynamicProxy<TProxyType>(obj);
		}
	}

	public abstract class DynamicProxy<T> : DynamicObject, INotifyPropertyChanged, IEquatable<DynamicProxy<T>>
		where T : class
	{
		private readonly T _obj;
		private Dictionary<string, Tuple<Func<DynamicProxy<T>, object>, Action<DynamicProxy<T>, object>>> _props = new Dictionary<string, Tuple<Func<DynamicProxy<T>, object>, Action<DynamicProxy<T>, object>>>();
		private Dictionary<string, Delegate> _methods = new Dictionary<string, Delegate>();
		private Dictionary<T, DynamicProxy<T>> _proxyCache = new Dictionary<T, DynamicProxy<T>>();
		private Dictionary<Type, object> _interfacesImplementation = new Dictionary<Type, object>();
		private readonly Dictionary<string, object> _propValues = new Dictionary<string, object>();


		public T Value
		{
			get { return _obj; }
		}

		public IEnumerable<DynamicProxy<T>> ChildProxies
		{
			get { return _proxyCache.Values.ToList(); }
		}

		[NotNull]
		public DynamicProxy<T> AddProperty<TPropertyType>([NotNull] string name, [NotNull] Func<object, TPropertyType> getter, [NotNull] Action<object, TPropertyType> setter)
		{
			if (name == null)
			{
				throw new ArgumentNullException("name");
			}
			if (getter == null)
			{
				throw new ArgumentNullException("getter");
			}
			if (setter == null)
			{
				throw new ArgumentNullException("setter");
			}
			_props[name] = new Tuple<Func<DynamicProxy<T>, object>, Action<DynamicProxy<T>, object>>(x => getter(x), (x, value) => setter(x, (TPropertyType)value));
			return this;
		}

		[NotNull]
		public DynamicProxy<T> AddProperty<TPropertyType>([NotNull] string name, TPropertyType defaultValue = default(TPropertyType))
		{
			if (name == null)
			{
				throw new ArgumentNullException("name");
			}
			AddProperty(name, x => (TPropertyType)((DynamicProxy<T>)x)._propValues.TryGet(name, defaultValue), (x, value) => ((DynamicProxy<T>)x)._propValues[name] = value);
			return this;
		}

		[NotNull]
		public DynamicProxy<T> AddMethod([NotNull] string name, [NotNull] Delegate action)
		{
			if (name == null)
			{
				throw new ArgumentNullException("name");
			}
			if (action == null)
			{
				throw new ArgumentNullException("action");
			}
			_methods[name] = action;
			return this;
		}

		[NotNull]
		public DynamicProxy<T> AddInterface<TInterface, TImplementation>([NotNull] TImplementation implementaion)
			where TImplementation : class, TInterface, IProxyInterfaceImplementaion<T>
		{
			if (implementaion == null)
			{
				throw new ArgumentNullException("implementaion");
			}
			_interfacesImplementation[typeof(TInterface)] = implementaion;
			return this;
		}

		protected DynamicProxy([NotNull] T obj)
		{
			if (obj == null)
			{
				throw new ArgumentNullException("obj");
			}
			_obj = obj;
			_proxyCache[obj] = this;
		}

        [DebuggerStepThrough]
		public override bool TryGetMember(GetMemberBinder binder, out object result)
		{
            try
            {
                result = _props.ContainsKey(binder.Name) ? _props[binder.Name].Item1(this) : _obj.GetProperty(binder.Name);
            }
            catch
            {
                result = _obj.GetType().InvokeMember(binder.Name, BindingFlags.GetProperty, null, _obj, null);
            }


		    result = GetResult(result);

			return true;
		}

		protected abstract DynamicProxy<T> CreateProxy(T obj);

		private DynamicProxy<T> InternalCreateProxy(T obj)
		{
			var proxy = CreateProxy(obj);
			proxy._props = _props;
			proxy._methods = _methods;
			proxy._proxyCache = _proxyCache;
			proxy._interfacesImplementation = _interfacesImplementation;
			return proxy;
		}

		private object GetResult(object result)
		{
			var createProxy = new Func<T, DynamicProxy<T>>(obj => _proxyCache.TryGet(obj, () => InternalCreateProxy(obj)));
			return (result as T).With(createProxy, (result as IEnumerable<T>).With(list => list.Select(createProxy), result));
		}

		public override bool TryConvert(ConvertBinder binder, out object result)
		{
			result = _interfacesImplementation.TryGet(binder.Type, _obj);
			(result as IProxyInterfaceImplementaion<T>).Do(x=>x.Activate(this));
			return true;
		}

        [DebuggerStepThrough]
		public override bool TrySetMember(SetMemberBinder binder, object value)
		{
			if (_props.ContainsKey(binder.Name))
			{
				_props[binder.Name].Item2(this, value);
			}
			else
			{
                try
                {
                    _obj.SetProperty(binder.Name, value);
                }
                catch
                {
                    _obj.GetType().InvokeMember(binder.Name, BindingFlags.SetProperty, null, _obj, new [] { value });
                }
			}

			PropertyChanged.Do(x => x(this, new PropertyChangedEventArgs(binder.Name)));

			return true;
		}

        [DebuggerStepThrough]
        private object InvokeNativeMethod(string name, object[] args)
        {
            try
            {
                return _obj.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, _obj, args);
            }
            catch
            {
                return _obj.GetType().InvokeMember(name, BindingFlags.GetProperty, null, _obj, args);
            }
        }

	    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
		{
            result = _methods.ContainsKey(binder.Name) ? _methods[binder.Name].DynamicInvoke(new[] { this }.Concat(args).ToArray()) : InvokeNativeMethod(binder.Name,args);

			result = GetResult(result);

			return true;
		}

		public bool Equals(DynamicProxy<T> other)
		{
			if (ReferenceEquals(null, other))
			{
				return false;
			}
			if (ReferenceEquals(this, other))
			{
				return true;
			}
			return Equals(other._obj, _obj) && Equals(other._props, _props) && Equals(other._methods, _methods) && Equals(other._proxyCache, _proxyCache) && Equals(other._propValues, _propValues);
		}

		public override bool Equals(object obj)
		{
			if (ReferenceEquals(null, obj))
			{
				return false;
			}
			if (ReferenceEquals(this, obj))
			{
				return true;
			}
			if (!(obj is DynamicProxy<T>))
			{
				return false;
			}
			return Equals((DynamicProxy<T>)obj);
		}

		public override int GetHashCode()
		{
			unchecked
			{
				var result = (_obj != null ? _obj.GetHashCode() : 0);
				result = (result * 397) ^ (_props != null ? _props.GetHashCode() : 0);
				result = (result * 397) ^ (_methods != null ? _methods.GetHashCode() : 0);
				result = (result * 397) ^ (_proxyCache != null ? _proxyCache.GetHashCode() : 0);
				result = (result * 397) ^ (_propValues != null ? _propValues.GetHashCode() : 0);
				return result;
			}
		}

		public static bool operator ==(DynamicProxy<T> left, DynamicProxy<T> right)
		{
			return Equals(left, right);
		}

		public static bool operator !=(DynamicProxy<T> left, DynamicProxy<T> right)
		{
			return !Equals(left, right);
		}

		public static implicit operator T(DynamicProxy<T> proxy)
		{
			return proxy._obj;
		}

		public static explicit operator DynamicProxy<T>(T obj)
		{
			return DynamicProxy.Create(obj);
		}

		public event PropertyChangedEventHandler PropertyChanged;
	}
}
